跳到主要内容

Go 为子进程添加代理

应用代理的原理

经常能看到一些代理程序可以只代理某个特定的程序,而非简单粗暴的全局代理,这是怎么做到的呢?

example

# 这个 proxychains4 是个很出名的代理工具,例如它代理后面这个 curl 
proxychains4 curl www.httpbin.org/ip

先来看下 Linux 正常的代理是怎么样的?

export http_proxy="http://proxy-XXXXX:port"
export https_proxy="https://proxy-XXXXX:port"

# 如果代理服务器需要用户名和密码才能访问,需要填写上面的username和passwd部分,否则的话,省略这两部分。
export https_proxy="http://username:password@proxyServer:port/"

# 设置不需要代理的地址
export no_proxy="127.0.0.1,192.168.124.0/16,*.example.com"

取消代理

unset http_proxy
unset https_proxy

注意, 上面这样设置环境变量(不是写在配置文件中的), 它只会作用于当前终端, 不会影响环境

所以利用这个特性就可以动态的给某个应用创建代理了, 使用 Golang 的 exec 工具包, 它创建 exec.Command(app) 一个命令等于开辟一个新的终端, 对环境并不影响

使用示例

编写一个代理服务端

package main

import "net/http"

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, Welcome to Baidu"))
}

func main() {
server := &server{}
http.ListenAndServe(":8080", server)
}

代理某个命令或者程序, 为它设置代理地址

package main

import (
"fmt"
"log"
"os"
"os/exec"
)

func main() {
command := exec.Command("curl", "http://baidu.com")

port := 8080
parentEnv := os.Environ()
// 设置代理环境变量
parentEnv = append(parentEnv, fmt.Sprintf("HTTP_PROXY=http://127.0.0.1:%d", port))
parentEnv = append(parentEnv, fmt.Sprintf("http_proxy=http://127.0.0.1:%d", port))
parentEnv = append(parentEnv, fmt.Sprintf("HTTPS_PROXY=http://127.0.0.1:%d", port))
parentEnv = append(parentEnv, fmt.Sprintf("https_proxy=http://127.0.0.1:%d", port))

command.Env = parentEnv
command.Stdout = os.Stdout
command.Stderr = os.Stdout

if err := command.Run(); err != nil {
if _, isExist := err.(*exec.ExitError); !isExist {
log.Fatalln("启动待测试程序失败, err:", err)
}
}

}

执行输出就为:

Hello, Welcome to Baidu

这是什么原理呢?回答这个问题之前先思考设置了代理,Golang 的 http 库是在哪里读取环境变量的呢?

答:在 http 包的 Transport 里面定义的 Proxy 函数里调用的,可以看到默认的这个 Transport 变量,里面使用 ProxyFromEnvironment 函数去读取环境变量

// net/http/transport.go
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

// ...

func ProxyFromEnvironment(req *Request) (*url.URL, error) {
return envProxyFunc()(req.URL)
}

// ...
func envProxyFunc() func(*url.URL) (*url.URL, error) {
envProxyOnce.Do(func() {
envProxyFuncValue = httpproxy.FromEnvironment().ProxyFunc()
})
return envProxyFuncValue
}